package ms.maplestory.boot.loaders;

import lombok.extern.slf4j.Slf4j;
import ms.maplestory.boot.ServerConstants;
import ms.maplestory.boot.client.character.skills.Skill;
import ms.maplestory.boot.client.character.skills.info.SkillInfo;
import ms.maplestory.boot.enums.MobSkillStatType;
import ms.maplestory.boot.enums.SkillStatType;
import ms.maplestory.boot.loaders.container.MobSkillInfo;
import ms.maplestory.boot.util.Position;
import ms.maplestory.boot.util.Rect;
import ms.maplestory.boot.util.Util;
import ms.maplestory.boot.util.XMLApi;
import org.w3c.dom.Node;

import java.io.*;
import java.util.*;

import static ms.maplestory.boot.loaders.MainLoader.INFO_OUT;

@Slf4j
public class SkillData {

    private static Map<Integer, SkillInfo> skills = new HashMap<>();
    private static Map<Integer, Map<Integer, MobSkillInfo>> mobSkillInfos = new HashMap<>();

    public static void generateDatFiles() {
        log.info("Started generating skill data.");
        long start = System.currentTimeMillis();
        loadSkillsFromWz();
        saveSkillsToDat(ServerConstants.DAT_DIR + "/skills");
        log.info(String.format("Completed generating skill data in %dms.", System.currentTimeMillis() - start));
        log.info("Started generating mob skill data.");
        start = System.currentTimeMillis();
        loadMobSkillsFromWz();
        saveMobSkillsToDat(ServerConstants.DAT_DIR + "/mobSkills");
        log.info(String.format("Completed generating mob skill data in %dms.", System.currentTimeMillis() - start));
    }

    private static void loadSkillsFromWz() {
        String wzDir = ServerConstants.WZ_DIR + "/Skill.wz";
        File dir = new File(wzDir);
        File[] files = dir.listFiles();
        for (File file : files) {
            if (file.getName().contains("Dragon")) {
                continue;
            }
            Node node = XMLApi.getRoot(file);
            if (node == null) {
                continue;
            }
            List<Node> nodes = XMLApi.getAllChildren(node);
            for (Node mainNode : nodes) {
                Map<String, String> attributes = XMLApi.getAttributes(mainNode);
                String rootIdStr = attributes.get("name").replace(".img", "");
                int rootId;
                if (Util.isNumber(rootIdStr)) {
                    rootId = Integer.parseInt(rootIdStr);
                } else {
                    continue;
                }
                Set<String> unkVals = new HashSet<>();
                Node skillChild = XMLApi.getFirstChildByNameBF(mainNode, "skill");
                for (Node skillNode : XMLApi.getAllChildren(skillChild)) {
                    Map<String, String> skillAttributes = XMLApi.getAttributes(skillNode);
                    String skillIdStr = skillAttributes.get("name").replace(".img", "");
                    int skillId;
                    if (Util.isNumber(skillIdStr)) {
                        SkillInfo skill = new SkillInfo();
                        skill.setRootId(rootId);
                        if (Util.isNumber(skillIdStr)) {
                            skillId = Integer.parseInt(skillIdStr);
                            skill.setId(skillId);
                        } else {
                            if (INFO_OUT)
                                log.warn(skillIdStr + " is not a number.");
                            continue;
                        }
                        for (Node mainLevelNode : XMLApi.getAllChildren(skillNode)) {
                            String mainName = XMLApi.getNamedAttribute(mainLevelNode, "name");
                            String mainValue = XMLApi.getNamedAttribute(mainLevelNode, "value");
                            int intVal = -1337;
                            if (Util.isNumber(mainValue)) {
                                intVal = Integer.parseInt(mainValue);
                            }
                            switch (mainName) {
                                case "masterLevel":
                                    skill.setMasterLevel(intVal);
                                    break;
                                case "invisible":
                                    skill.setInvisible(intVal != 0);
                                    break;
                                case "type":
                                    skill.setType(intVal);
                                    break;
                                case "psd":
                                    skill.setPsd(intVal != 0);
                                    break;
                                case "psdSkill":
                                    for (Node psdSkillNode : XMLApi.getAllChildren(mainLevelNode)) {
                                        skill.addPsdSkill(Integer.parseInt(XMLApi.getAttributes(psdSkillNode).get("name")));
                                    }
                                    break;
                                case "elemAttr":
                                    skill.setElemAttr(mainValue);
                                    break;
                                case "req":
                                    for (Node reqChild : XMLApi.getAllChildren(mainLevelNode)) {
                                        String childName = XMLApi.getNamedAttribute(reqChild, "name");
                                        String childValue = XMLApi.getNamedAttribute(reqChild, "value");
                                        if (Util.isNumber(childName)) {
                                            skill.addReqSkill(Integer.parseInt(childName), Integer.parseInt(childValue));
                                        }
                                    }
                                    break;
                                case "common":
                                case "info":
                                case "level":
                                    for (Node commonNode : XMLApi.getAllChildren(mainLevelNode)) {
                                        Map<String, String> commonAttr = XMLApi.getAttributes(commonNode);
                                        String nodeName = commonAttr.get("name");
                                        if ("maxLevel".equals(nodeName)) {
                                            skill.setMaxLevel(Integer.parseInt(XMLApi.getNamedAttribute(commonNode, "value")));
                                        } else if (nodeName.contains("lt") && nodeName.length() <= 3) {
                                            Node rbNode = XMLApi.getFirstChildByNameBF(mainLevelNode, nodeName.replace("lt", "rb"));
                                            int left = Integer.parseInt(XMLApi.getNamedAttribute(commonNode, "x"));
                                            int top = Integer.parseInt(XMLApi.getNamedAttribute(commonNode, "y"));
                                            int right = Integer.parseInt(XMLApi.getNamedAttribute(rbNode, "x"));
                                            int bottom = Integer.parseInt(XMLApi.getNamedAttribute(rbNode, "y"));
                                            skill.addRect(new Rect(left, top, right, bottom));
                                        } else {
                                            SkillStatType skillStat = SkillStatType.getSkillStatByString(nodeName);
                                            if (skillStat != null) {
                                                skill.addSkillStatInfo(skillStat, commonAttr.get("value"));
                                            } else if (!unkVals.contains(nodeName)) {
                                                if (INFO_OUT)
                                                    log.warn("Unknown SkillStat " + nodeName);
                                                unkVals.add(nodeName);
                                            }
                                        }
                                    }
                                    break;
                            }
                        }
                        skills.put(skillId, skill);
                    }
                }
            }
        }
    }

    public static void loadMobSkillsFromWz() {
        String wzDir = ServerConstants.WZ_DIR + "/Skill.wz/MobSkill.img.xml";
        File file = new File(wzDir);
        Node root = XMLApi.getRoot(file);
        Node mainNode = XMLApi.getAllChildren(root).get(0);
        List<Node> nodes = XMLApi.getAllChildren(mainNode);
        Set<String> unks = new HashSet<>();
        for (Node node : nodes) {
            int skillID = Integer.parseInt(XMLApi.getNamedAttribute(node, "name"));
            for (Node levelNode : XMLApi.getAllChildren(XMLApi.getFirstChildByNameBF(node, "level"))) {
                int level = Integer.parseInt(XMLApi.getNamedAttribute(levelNode, "name"));
                MobSkillInfo msi = new MobSkillInfo();
                msi.setId(skillID);
                msi.setLevel(level);
                for (Node skillStatNode : XMLApi.getAllChildren(levelNode)) {
                    String name = XMLApi.getNamedAttribute(skillStatNode, "name");
                    String value = XMLApi.getNamedAttribute(skillStatNode, "value");
                    String x = XMLApi.getNamedAttribute(skillStatNode, "x");
                    String y = XMLApi.getNamedAttribute(skillStatNode, "y");
                    if (Util.isNumber(name)) {
                        msi.addIntToList(Integer.parseInt(value));
                        continue;
                    }
                    switch (name) {
                        case "x":
                            msi.putMobSkillStat(MobSkillStatType.x, value);
                            break;
                        case "mpCon":
                            msi.putMobSkillStat(MobSkillStatType.mpCon, value);
                            break;
                        case "interval":
                        case "inteval":
                            msi.putMobSkillStat(MobSkillStatType.interval, value);
                            break;
                        case "hp":
                        case "HP":
                            msi.putMobSkillStat(MobSkillStatType.hp, value);
                            break;
                        case "info":
                            msi.putMobSkillStat(MobSkillStatType.info, value);
                            break;
                        case "y":
                            msi.putMobSkillStat(MobSkillStatType.y, value);
                            break;
                        case "lt":
                            msi.setLt(new Position(Integer.parseInt(x), Integer.parseInt(y)));
                            break;
                        case "rb":
                            msi.setRb(new Position(Integer.parseInt(x), Integer.parseInt(y)));
                            break;
                        case "limit":
                            msi.putMobSkillStat(MobSkillStatType.limit, value);
                            break;
                        case "z":
                            msi.putMobSkillStat(MobSkillStatType.z, value);
                            break;
                        case "prop":
                            msi.putMobSkillStat(MobSkillStatType.prop, value);
                            break;
                        case "count":
                            msi.putMobSkillStat(MobSkillStatType.count, value);
                            break;
                        case "time":
                            msi.putMobSkillStat(MobSkillStatType.time, value);
                            break;
                        case "elemAttr":
                            msi.putMobSkillStat(MobSkillStatType.elemAttr, value);
                            break;
                        case "delay":
                            msi.putMobSkillStat(MobSkillStatType.delay, value);
                            break;
                        case "summonEffect":
                            msi.putMobSkillStat(MobSkillStatType.summonEffect, value);
                            break;
                        case "level":
                            msi.putMobSkillStat(MobSkillStatType.level, value);
                            break;
                        case "effect":
                        case "mob":
                        case "mob0":
                        case "hit":
                        case "affected":
                        case "affectedOtherSkill":
                        case "crash":
                        case "effectToUser":
                        case "affected_after":
                        case "fixDamR":
                        case "limitMoveSkill":
                        case "tile":
                        case "footholdRect":
                        case "targetMobType":
                        case "areaWarning":
                        case "arType":
                        case "tremble":
                        case "otherSkill":
                        case "etcEffect":
                        case "etcEffect1":
                        case "etcEffect2":
                        case "etcEffect3":
                        case "bombInfo":
                        case "affected_pre":
                        case "fixDamR_BT":
                        case "affectedPhase":
                        case "screen":
                        case "notMissAttack":
                        case "ignoreEvasion":
                        case "fadeinfo":
                        case "randomTarget":
                        case "option_linkedMob":
                        case "affected0":
                        case "summonOnce":
                        case "head":
                        case "mobGroup":
                        case "exceptRange":
                        case "exchangeAttack":
                        case "range":
                        case "addDam":
                        case "special":
                        case "target":
                        case "fixedPos":
                        case "fixedDir":
                        case "i52":
                        case "start":
                        case "cancleType":
                        case "succeed":
                        case "failed":
                        case "during":
                        case "castingBarHide":
                        case "skillCancelAlways":
                        case "cancleDamage":
                        case "cancleDamageMultiplier":
                        case "bounceBall":
                        case "info2":
                        case "regen":
                        case "kockBackD":
                        case "areaSequenceDelay":
                        case "areaSequenceRandomSplit":
                        case "accelerationEffect":
                        case "repeatEffect":
                        case "brightness":
                        case "brightnessDuration":
                        case "success":
                        case "fail":
                        case "affected_S":
                        case "appear":
                        case "affected_XS":
                        case "disappear":
                        case "command":
                        case "damIncPos": // May be useful
                        case "option_poison": // ?
                        case "phaseUserCount": // I think this is done client side (users hit mapped to phase?)
                            break;
                        default:
                            if (!unks.contains(name)) {
                                if (INFO_OUT)
                                    log.warn(String.format("Unkown MobSkillStat %s with value %s (skill %d level %d)", name, value, skillID, level));
                                unks.add(name);
                            }
                            break;
                    }
                }
                addMobSkillInfo(msi);
            }
        }

    }

    public static void saveSkillsToDat(String dir) {
        Util.makeDirIfAbsent(dir);
        for (Map.Entry<Integer, SkillInfo> entry : skills.entrySet()) {
            SkillInfo si = entry.getValue();
            File file = new File(String.format("%s/%d.dat", dir, si.getId()));
            try (DataOutputStream dataOutputStream = new DataOutputStream(new FileOutputStream(file))) {
                dataOutputStream.writeInt(si.getId());
                dataOutputStream.writeInt(si.getRootId());
                dataOutputStream.writeInt(si.getMaxLevel());
                dataOutputStream.writeInt(si.getMasterLevel());
                dataOutputStream.writeBoolean(si.isInvisible());
                dataOutputStream.writeInt(si.getType());
                dataOutputStream.writeUTF(si.getElemAttr());
                dataOutputStream.writeBoolean(si.isPsd());
                dataOutputStream.writeShort(si.getSkillStatInfo().size());
                for (Map.Entry<SkillStatType, String> ssEntry : si.getSkillStatInfo().entrySet()) {
                    dataOutputStream.writeUTF(ssEntry.getKey().toString());
                    if (ssEntry.getValue() == null) {
                        dataOutputStream.writeUTF("");
                    } else {
                        dataOutputStream.writeUTF(ssEntry.getValue());
                    }
                }
                dataOutputStream.writeShort(si.getRectList().size());
                for (Rect r : si.getRectList()) {
                    dataOutputStream.writeInt(r.getLeft());
                    dataOutputStream.writeInt(r.getTop());
                    dataOutputStream.writeInt(r.getRight());
                    dataOutputStream.writeInt(r.getBottom());
                }
                dataOutputStream.writeShort(si.getPsdSkills().size());
                for (int i : si.getPsdSkills()) {
                    dataOutputStream.writeInt(i);
                }
                dataOutputStream.writeShort(si.getReqSkills().size());
                for (Map.Entry<Integer, Integer> reqSkill : si.getReqSkills().entrySet()) {
                    dataOutputStream.writeInt(reqSkill.getKey());
                    dataOutputStream.writeInt(reqSkill.getValue());
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void saveMobSkillsToDat(String dir) {
        Util.makeDirIfAbsent(dir);
        for (Map.Entry<Integer, Map<Integer, MobSkillInfo>> entry : mobSkillInfos.entrySet()) {
            int id = entry.getKey();
            for (Map.Entry<Integer, MobSkillInfo> entry2 : entry.getValue().entrySet()) {
                int level = entry2.getKey();
                MobSkillInfo msi = entry2.getValue();
                File file = new File(String.format("%s/%d-%d.dat", dir, id, level));
                try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(file))) {
                    dos.writeInt(msi.getId());
                    dos.writeInt(msi.getLevel());
                    // wow this is ugly
                    // but it's just a de/encoder, so who cares
                    boolean hasLt = msi.getLt() != null;
                    dos.writeBoolean(hasLt);
                    if (hasLt) {
                        dos.writeInt(msi.getLt().getX());
                        dos.writeInt(msi.getLt().getY());
                    }
                    boolean hasRb = msi.getRb() != null;
                    dos.writeBoolean(hasRb);
                    if (hasRb) {
                        dos.writeInt(msi.getRb().getX());
                        dos.writeInt(msi.getRb().getY());
                    }
                    dos.writeShort(msi.getMobSkillStats().size());
                    for (Map.Entry<MobSkillStatType, String> msiString : msi.getMobSkillStats().entrySet()) {
                        dos.writeByte(msiString.getKey().ordinal());
                        dos.writeUTF(msiString.getValue());
                    }
                    dos.writeShort(msi.getInts().size());
                    for (int i : msi.getInts()) {
                        dos.writeInt(i);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void addMobSkillInfo(MobSkillInfo msi) {
        mobSkillInfos.computeIfAbsent(msi.getId(), k -> new HashMap<>());
        Map<Integer, MobSkillInfo> msiLevelMap = mobSkillInfos.get(msi.getId());
        msiLevelMap.put(msi.getLevel(), msi);
        mobSkillInfos.put(msi.getId(), msiLevelMap);
    }

    public static Skill getSkillDeepCopyById(int skillId) {
        SkillInfo si = getSkillInfoById(skillId);
        if (si == null) {
            return null;
        }
        Skill skill = new Skill();
        skill.setSkillId(si.getId());
        skill.setMasterLevel(si.getMaxLevel());
        skill.setMaxLevel(si.getMaxLevel());
        if (si.getMasterLevel() <= 0) {
            skill.setMasterLevel(skill.getMaxLevel());
        }
        skill.setCurrentLevel(0);
        return skill;
    }

    public static SkillInfo getSkillInfoById(int skillId) {
        if (!skills.containsKey(skillId)) {
            File file = new File(String.format("%s/skills/%d.dat", ServerConstants.DAT_DIR, skillId));
            if (file.exists()) {
                loadSkillFromDat(file);
            }
        }
        return skills.get(skillId);
    }

    public static void loadSkillFromDat(File file) {
        try (DataInputStream dataInputStream = new DataInputStream(new FileInputStream(file))) {
            SkillInfo skillInfo = new SkillInfo();
            skillInfo.setId(dataInputStream.readInt());
            skillInfo.setRootId(dataInputStream.readInt());
            skillInfo.setMaxLevel(dataInputStream.readInt());
            skillInfo.setMasterLevel(dataInputStream.readInt());
            skillInfo.setInvisible(dataInputStream.readBoolean());
            skillInfo.setType(dataInputStream.readInt());
            skillInfo.setElemAttr(dataInputStream.readUTF());
            skillInfo.setPsd(dataInputStream.readBoolean());
            short ssSize = dataInputStream.readShort();
            for (int j = 0; j < ssSize; j++) {
                skillInfo.addSkillStatInfo(SkillStatType.getSkillStatByString(
                        dataInputStream.readUTF()), dataInputStream.readUTF());
            }
            short rectSize = dataInputStream.readShort();
            for (int j = 0; j < rectSize; j++) {
                int left = dataInputStream.readInt();
                int top = dataInputStream.readInt();
                int right = dataInputStream.readInt();
                int bottom = dataInputStream.readInt();
                skillInfo.addRect(new Rect(left, top, right, bottom));
            }
            short psdSize = dataInputStream.readShort();
            for (int j = 0; j < psdSize; j++) {
                skillInfo.addPsdSkill(dataInputStream.readInt());
            }
            short reqSkillSize = dataInputStream.readShort();
            for (int j = 0; j < reqSkillSize; j++) {
                skillInfo.addReqSkill(dataInputStream.readInt(), dataInputStream.readInt());
            }
            skills.put(skillInfo.getId(), skillInfo);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}
